// *********************************************************
//      Copyright (c) Kosta Gaitanis
//      All rights reserved
//
//      k.gaitanis@gmail.com
//
// *********************************************************
#include <cmath>
#include "World.h"

World::World() :
    m_world(new Item("World")),
    m_camera(new Camera())
{
}

World::~World()
{
    // delete all the textures first
    foreach (const Texture* texture, getTextures()) {
        Texture* t = const_cast<Texture*>(texture);
        delete t;
    }

    delete m_world;
    delete m_camera;
    qDeleteAll(m_lights);
}

Item *World::getWorldItem() const
{
    return m_world;
}

void World::addItem(Item *item)
{
    m_world->addChild(item);
}

bool World::hasItem(Item *item, bool recursive) const
{
    return m_world->hasChild(item, recursive);
}

void World::addLight(Light *light)
{
    if (m_lights.contains(light)) {
        qDebug() << "Light has already been added to world.";
        return;
    }

    m_lights.append(light);
}

void World::addTexture(const Texture *texture)
{
    if (texture == 0) {
        return;
    }
    if (m_textures.contains(texture)) {
        qDebug() << "Texture" << texture->getName() << "already exists in the word.";
        return;
    }
    m_textures.append(texture);
}

void World::bindTexture(const Texture *texture, Item *item)
{
    if (!m_textures.contains(texture)) {
        addTexture(texture);
    }

    if (!m_world->hasChild(item, true)) {
        qDebug() << "World does not contain item" << item->getName();
        return;
    }
    item->setTexture(texture);
}

bool World::hasTexture(const Texture *texture) const
{
    return m_textures.contains(texture);
}

Light *World::getLight(int index) const
{
    return m_lights.value(index, (Light*) 0);
}

QList<const Texture *> World::getTextures() const
{
    return m_textures;
}

const Texture *World::getTexture(const QString &name) const
{
    foreach (const Texture* tex, m_textures) {
        if (tex->getName() == name) {
            return tex;
        }
    }
    return 0;
}

Camera *World::getCamera() const
{
    return m_camera;
}

void World::paint()
{
    m_camera->initializeGL();

    // set up the model view matrix
    m_camera->paint();

    // paint the lights
    foreach (Light* light, m_lights) {
        light->paint();
    }

    // paint everything
    paintItems(FarToNear);
}

void World::initializeGL()
{
    if (glGetError()) {
        qDebug() << "There was an error somewhere before initializing the world.";
    }

    //START//
    // enable blending
    glEnable (GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // alow using glColor instead of material
    //glEnable(GL_COLOR_MATERIAL);

    // enable and setup depth test
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glEnable(GL_TEXTURE_2D);
    //END//

    // setup rendering hints :
    // the quality of color and texture coordinate interpolation.
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    // set up the lights
    if (m_lights.count()) {
        //START//
        // enable lighting
        glEnable(GL_LIGHTING);
        //END//
    }
    foreach (Light* light, m_lights) {
        light->initializeGL();
    }

    // Initialize Textures
    foreach (const Texture* texture, m_textures) {
        Texture* tex = const_cast<Texture*>(texture);
        tex->initializeGL();
    }

    // initialize projection matrix
    m_camera->initializeGL();

    // initialize paint lists for all items
    m_world->initializeGL();

    if (glGetError()) {
        qDebug() << "There was an error while initializing the world.";
    }
}

void World::paintItems(World::PaintMethod method)
{
    if (method == Recursive) {
        paintItemRecursively(m_world);

    } else if (method == FarToNear) {
        QList<PaintItem*> items = getPaintItems(m_world); 
        qSort(items.begin(), items.end(), ItemDistanceMoreThan());

        foreach (PaintItem *item, items) {
            // push the current transformation matrix
            glPushMatrix();

            // multiply by this item's transformation matrix
            GLdouble m[16];
            item->transform.toGLArray(m);
            glMultMatrixd(m);

            // paint the item
            item->item->paint(getCamera());

            // go back to the previous transformation matrix
            glPopMatrix();
        }

        // delete the paintitems. This does not affect the real items.
        qDeleteAll(items);
    }
}

void World::paintItemRecursively(Item *item)
{
    // dont paint invisible items and dont paint their children
    if (!item->isVisible()) {
        return;
    }

    //START//    
    // push the current transformation matrix
    glPushMatrix();
    // multiply by this item's transformation matrix m_glTransform
    glMultMatrixd(item->getGLTransform());
    // paint the faces of this item
    item->paint(getCamera());

    // do the same for all the child items
    foreach(Item* child, item->getChildren()) {
        paintItemRecursively(child);
    }

    // go back to the previous transformation matrix
    glPopMatrix();
    //END//
}

QList<PaintItem*> World::getPaintItems(Item *item, Matrix transform) const
{    
    QList<PaintItem*> items;

    // dont paint invisible items and dont paint their children
    if (!item->isVisible()) {
        return items;
    }

    Matrix m = transform * item->getTransform();
    Vector itemPos = m * Vector::ZERO;
    double d = (m_camera->getCameraPosition() - itemPos).getLength();

    if (item->getMaterial()->getDiffuse().alphaF() == 1.0) {
        // this item is opaque. Paint it now!
        // push the current transformation matrix
        glPushMatrix();

        // multiply by this item's global transformation matrix
        GLdouble mm[16];
        m.toGLArray(mm);
        glMultMatrixd(mm);

        // paint the item
        item->paint(getCamera());

        // go back to the previous transformation matrix
        glPopMatrix();
    } else {
        // this item is transparent. Add to the list and paint it later
        items.append(new PaintItem(item, d, m));
    }
    foreach (Item* child, item->getChildren()) {
        items.append(getPaintItems(child, m));
    }

    return items;
}

bool ItemDistanceMoreThan::operator()(PaintItem *item1, PaintItem *item2) const
{
    return item1->distance > item2->distance;
}


PaintItem::PaintItem(Item *item, double distance, Matrix transform) :
    item(item),
    distance(distance),
    transform(transform)
{
}
